/*
 * MIT License
 *
 * Copyright (c) 2021 Mr.css
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in all
 * copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
 * SOFTWARE.
 */

package cn.seaboot.excel;

import cn.seaboot.commons.core.Asserts;
import cn.seaboot.commons.lang.Warning;
import cn.seaboot.excel.call.RowReadCallback;
import cn.seaboot.excel.call.RowReaderAndWriter;
import cn.seaboot.excel.call.RowWriteCallback;
import cn.seaboot.excel.call.StartupCallback;
import cn.seaboot.excel.header.Header;
import cn.seaboot.excel.style.DefaultStyleProvider;
import cn.seaboot.excel.style.StyleProvider;
import org.apache.poi.EncryptedDocumentException;
import org.apache.poi.ss.usermodel.*;
import org.apache.poi.ss.util.CellRangeAddress;
import org.apache.poi.xssf.streaming.SXSSFWorkbook;
import org.apache.poi.xssf.usermodel.XSSFSheet;
import org.apache.poi.xssf.usermodel.XSSFWorkbook;
import org.jetbrains.annotations.NotNull;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.io.Closeable;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.util.function.Consumer;

/**
 * 07 版本的 Excel 工具
 * <p>
 * HSSFWorkbook         03版本
 * XSSFWorkbook         07版本
 * StreamingWorkbook    超大文件读取
 * SXSSFWorkbook        超大文件写入
 * <p>
 * WorkBook：工作簿，一整个 Excel；
 * Sheet：工作表，一份 Excel 可以包含多个工作表；
 * Row：行，每一个表格可以包含很多行；
 * Cell：单元格，每一行包含很多单元格。
 * <p>
 * {@link Workbook}的封装类，主要针对Excel导入导出业务，函数调用上会更加优雅
 *
 * @author Mr.css
 * @version 2022-02-24 10:44
 * @since 2022-12-15 删除泛型，避免产生奇怪的代码警告，使用的时候需要注意对象类型的设置
 */
public class ExcelBook implements Iterable<Row>, Closeable {
    private final static Logger logger = LoggerFactory.getLogger(ExcelBook.class);

    /**
     * 样式提供器，一个方便二次封装的接口
     */
    private final static StyleProvider DEFAULT_STYLE_PROVIDER = new DefaultStyleProvider();

    /**
     * workbook 工作簿
     */
    private Workbook workbook;
    /**
     * sheet 工作表
     */
    private Sheet sheet;
    /**
     * read or write start row number 起始行号
     */
    private Integer startRow;
    /**
     * read last row number 结束行号
     */
    private Integer endRow;
    /**
     * 当前样式
     * <p>
     * ExcelBook 写入数据的时候，将样式设置到单元格中。
     * <p>
     * 注意：新版代码，不会将 style 回传到读写回调中。
     */
    private CellStyle style;

    /**
     * 样式提供器
     */
    private StyleProvider styleProvider = DEFAULT_STYLE_PROVIDER;
    /**
     * 数据读回调
     */
    @SuppressWarnings(Warning.RAW_TYPES)
    private RowReadCallback reader;
    /**
     * 数据写回调
     */
    @SuppressWarnings(Warning.RAW_TYPES)
    private RowWriteCallback writer;

    /**
     * 创建一个工作簿，并创建一个工作表 (sheet)
     * <p>
     * Create workbook， always create default sheet, I think a workbook without sheet is useless.
     *
     * @return -
     */
    public static ExcelBook create() {
        ExcelBook builder = new ExcelBook();
        builder.workbook = new XSSFWorkbook();
        builder.sheet = builder.workbook.createSheet();
        return builder;
    }

    /**
     * 创建一个工作簿，并创建一个工作表 (sheet)
     * <p>
     * Create workbook， always create default sheet, I think a workbook without sheet is useless.
     *
     * @param sheetName 工作表名，注意这个不是文件地址
     * @return -
     */
    public static ExcelBook create(String sheetName) {
        ExcelBook builder = new ExcelBook();
        builder.workbook = new XSSFWorkbook();
        builder.sheet = builder.workbook.createSheet(sheetName);
        return builder;
    }

    /**
     * 从输入流中，打开一份工作簿。输入流中，一般是一个文件模版，或者是已经有数据的文件
     * <p>
     * Create workbook by a excel stream.
     *
     * @param is is
     * @return -
     */
    public static ExcelBook create(InputStream is) {
        try {
            ExcelBook builder = new ExcelBook();
            builder.workbook = WorkbookFactory.create(is);
            return builder;
        } catch (EncryptedDocumentException | IOException e) {
            throw new ExcelException("read excel failed!check your file!", e);
        }
    }

    /**
     * 大文件写入
     * <p>
     * 调一下这个函数，将 Workbook 切换 {@link SXSSFWorkbook} ，数据写入的方式不变，
     * <p>
     * builder.workbook convert to SXSSFWorkbook, start more slowly, but use less memory.
     * this workbook can be used to deal with large excel when writing.
     *
     * @param rowAccessWindowSize - the number of rows that are kept in memory until flushed out, see above.
     * @return ExcelBuilder
     * @see SXSSFWorkbook
     * @see LargerExcelReader
     * @since Excel.2007 (.xlsx)
     */
    public ExcelBook asSXSSFWorkbook(int rowAccessWindowSize) {
        this.workbook = new SXSSFWorkbook((XSSFWorkbook) this.workbook, rowAccessWindowSize);
        return this;
    }

    /**
     * 尝试将 workbook 转换为 {@link XSSFWorkbook}
     * <p>
     * 水印、批注等功能，需要 2007 之后的版本
     *
     * @return {@link XSSFWorkbook} 转换失败的时候，返回null
     * @throws ExcelException 转换失败的时候
     * @since Excel 2007
     */
    @NotNull
    public XSSFWorkbook getXSSFWorkbook() {
        if (workbook instanceof XSSFWorkbook) {
            return (XSSFWorkbook) workbook;
        } else if (workbook instanceof SXSSFWorkbook) {
            return ((SXSSFWorkbook) workbook).getXSSFWorkbook();
        } else {
            throw new ExcelException("Workbook is not instance of XSSFWorkbook: " + workbook.getClass());
        }
    }

    /**
     * 尝试将工作表对象转换为 {@link XSSFSheet}
     * <p>
     * 水印、批注等功能，需要 2007 之后的版本
     *
     * @return {@link XSSFSheet}
     * @throws ExcelException 转换失败的时候
     * @since Excel 2007
     */
    @NotNull
    public XSSFSheet getXSSFSheet() {
        if (this.sheet instanceof XSSFSheet) {
            return (XSSFSheet) this.sheet;
        } else if (workbook instanceof SXSSFWorkbook) {
            return ((SXSSFWorkbook) workbook).getXSSFWorkbook().getSheet(this.sheet.getSheetName());
        } else {
            throw new ExcelException("Sheet is not instance of XSSFSheet: " + workbook.getClass());
        }
    }

    /**
     * 返回当前使用的 {@link Workbook} ，可能程序员想自己操作 Excel。
     *
     * @return Sheet
     */
    public Workbook getWorkbook() {
        return this.workbook;
    }

    /**
     * Get the Sheet object at the given index.
     * <p>
     * 设置当前使用的工作表
     *
     * @param idx of the sheet number (0-based physical & logical)
     * @return -
     */
    public ExcelBook sheetAt(int idx) {
        sheet = workbook.getSheetAt(idx);
        return this;
    }

    /**
     * Get sheet with the given name
     * <p>
     * 设置当前使用的工作表
     *
     * @param name of the sheet
     * @return -
     */
    public ExcelBook sheetAt(String name) {
        sheet = workbook.getSheet(name);
        return this;
    }

    /**
     * 创建并切换到指定工作表，如果已经存在，则使用之前的工作簿
     *
     * @param idx of the sheet number (0-based physical & logical)
     * @return -
     */
    public ExcelBook createSheetAt(int idx) {
        if (idx >= workbook.getNumberOfSheets()) {
            sheet = workbook.createSheet();
        } else {
            sheet = workbook.getSheetAt(idx);
        }
        return this;
    }


    /**
     * 创建并切换到指定工作表，如果已经存在，则使用之前的工作簿
     *
     * @param name of the sheet
     * @return -
     */
    public ExcelBook createSheetAt(String name) {
        sheet = workbook.getSheet(name);
        if (sheet == null) {
            sheet = workbook.createSheet(name);
        }
        return this;
    }

    /**
     * 创建一个工作表
     *
     * @return -
     */
    public ExcelBook createSheet() {
        workbook.createSheet();
        return this;
    }

    /**
     * 创建一个工作表
     *
     * @param name of the sheet
     * @return -
     */
    public ExcelBook createSheet(String name) {
        workbook.createSheet(name);
        return this;
    }

    /**
     * 返回当前使用的 {@link Sheet} ，可能程序员想自己操作 Excel。
     *
     * @return Sheet
     */
    public Sheet getSheet() {
        return this.sheet;
    }

    /**
     * 返回指定行号的 {@link Row} ，在制作表头的时候，会非常实用。
     *
     * @param idx Excel行号
     * @return Row
     */
    public Row getRow(int idx) {
        return this.sheet.getRow(idx);
    }

    /**
     * 获取单元格
     *
     * @param r 行号
     * @param c 列号
     * @return Cell
     */
    public Cell getCell(int r, int c) {
        Row row = sheet.getRow(r);
        if (row == null) {
            return null;
        } else {
            return row.getCell(c);
        }
    }


    /**
     * 设置单元格宽度
     * <p>
     * E.G.: 20*256 大约 20 字符的宽度
     *
     * @param idx   列号
     * @param width 列宽
     * @return this
     */
    public ExcelBook setColumnWidth(int idx, int width) {
        this.sheet.setColumnWidth(idx, width);
        return this;
    }

    /**
     * set read or write start row number
     *
     * @param rowNum row number
     * @return -
     */
    public ExcelBook setStartRow(int rowNum) {
        startRow = rowNum;
        return this;
    }

    /**
     * 获取起始行号
     *
     * @return 起始行号
     */
    public Integer getStartRow() {
        return startRow;
    }

    /**
     * 设置结束行号（读取范围）
     * <p>
     * 采用左闭右开的方式，如果希望读取 0-3 行，rowNum 的值需要传入 4
     * <p>
     * set last row number for reading data
     *
     * @return -
     */
    public ExcelBook setEndRow(int rowNum) {
        this.endRow = rowNum - 1;
        return this;
    }

    /**
     * 设置结束行号（读取范围）
     * <p>
     * 将有数据的最后一行作为结束行号。
     * <p>
     * Only in reading, set endRow as number of the last row which has data.
     *
     * @return -
     */
    public ExcelBook setLastRowAsEndRow() {
        endRow = sheet.getLastRowNum();
        return this;
    }

    /**
     * 设置结束行号（读取范围）
     * <p>
     * 将有数据的最后一行作为结束行号。
     * <p>
     * Only in reading, set endRow as number of the last row which has data.
     *
     * @return -
     */
    public ExcelBook setFirstRowAsStartRow() {
        startRow = sheet.getFirstRowNum();
        return this;
    }

    /**
     * 获取 Excel 中的总行数
     *
     * @return 行号
     */
    public int getLastRowNum() {
        return this.sheet.getLastRowNum();
    }

    /**
     * 获取结束行号
     *
     * @return 结束行号
     */
    public Integer getEndRow() {
        return endRow;
    }


    /**
     * 设置读写回调
     *
     * @param r 读写回调
     * @return this
     */
    public <T> ExcelBook setRowReader(RowReadCallback<T> r) {
        if (r instanceof StartupCallback) {
            ((StartupCallback) r).startup(this.workbook);
        }
        this.reader = r;
        return this;
    }

    /**
     * 设置读写回调
     *
     * @param w 读写回调
     * @return this
     */
    public <T> ExcelBook setRowWriter(RowWriteCallback<T> w) {
        if (w instanceof StartupCallback) {
            ((StartupCallback) w).startup(this.workbook);
        }
        this.writer = w;
        return this;
    }

    /**
     * 设置读写回调
     *
     * @param rw 读写回调
     * @return this
     */
    public <T> ExcelBook setRowReaderAndWriter(RowReaderAndWriter<T> rw) {
        rw.startup(this.workbook);
        this.writer = rw;
        this.reader = rw;
        return this;
    }

    /**
     * 单元格样式
     * <p>
     * 写入数据的时候，新建的单元格会采用当前样式
     *
     * @param style 单元格样式
     * @return this
     */
    public ExcelBook setStyle(CellStyle style) {
        this.style = style;
        return this;
    }

    /**
     * 获取当前样式
     *
     * @return CellStyle
     */
    public CellStyle getStyle() {
        return this.style;
    }

    /**
     * 获取指定单元格的样式
     * <p>
     * 数据导出的时候，如果用到文件模版，很经常用到这个函数
     *
     * @param row 行号
     * @param col 列号
     * @return CellStyle
     */
    public CellStyle getStyle(int row, int col) {
        return this.sheet.getRow(row).getCell(col).getCellStyle();
    }


    /**
     * 获取样式数组
     * <p>
     * 数据导出的时候，如果用到文件模版，很经常用到这个函数
     *
     * @param row 行号
     * @return CellStyle
     */
    public CellStyle[] getStyles(int row) {
        Row r = this.sheet.getRow(row);
        CellStyle[] ret = new CellStyle[r.getPhysicalNumberOfCells()];
        for (Cell cell : r) {
            ret[cell.getColumnIndex()] = cell.getCellStyle();
        }
        return ret;
    }

    /**
     * 在 Excel 中构建一个基础样式对象
     * <p>
     * {@code CellStyle} 需要由 workbook 构建
     *
     * @return CellStyle
     */
    public CellStyle createCellStyle() {
        return this.workbook.createCellStyle();
    }

    /**
     * 复制单元格样式
     * <p>
     * 场景：做数据导出的时候，可以先起一行数据样例，所有数据复制那一行的样式。
     * <p>
     * Clone style from Excel cell, this style will use for creating new cell.
     *
     * @param row 行号
     * @param col 列号
     * @return this
     */
    public ExcelBook cloneStyle(int row, int col) {
        this.style = this.sheet.getRow(row).getCell(col).getCellStyle();
        return this;
    }

    /**
     * 置空样式
     * <p>
     * 使用默认样式写入数据
     *
     * @return this
     */
    public ExcelBook removeStyle() {
        this.style = null;
        return this;
    }

    /**
     * 通过 name 从 provider 中获取单元格样式
     * <p>
     * 这个样式会被用于构建新的单元格
     *
     * @param name 单元格样式名称
     * @return this
     */
    public ExcelBook setStyle(@NotNull String name) {
        this.style = this.styleProvider.getStyle(name);
        return this;
    }

    /**
     * 设置样式提供者
     *
     * @param provider 样式提供者
     */
    public ExcelBook setStyleProvider(StyleProvider provider) {
        provider.initialize(this.workbook);
        this.styleProvider = provider;
        return this;
    }

    /**
     * 合并单元格
     *
     * @param region {@link Sheet#addMergedRegion(CellRangeAddress)}
     * @return this
     */
    public ExcelBook merge(CellRangeAddress region) {
        sheet.addMergedRegion(region);
        return this;
    }

    /**
     * 合并单元格
     * Creates new cell range. Indexes are zero-based.
     *
     * @param firstRow Index of first row
     * @param lastRow  Index of last row (inclusive), must be equal to or larger than {@code firstRow}
     * @param firstCol Index of first column
     * @param lastCol  Index of last column (inclusive), must be equal to or larger than {@code firstCol}
     * @return this
     */
    public ExcelBook merge(int firstRow, int lastRow, int firstCol, int lastCol) {
        return this.merge(new CellRangeAddress(firstRow, lastRow, firstCol, lastCol));
    }

    /**
     * 从迭代器中读取 Excel 内容，读取内容取决于代码指定的范围。
     * 会因为异常中断数据读取。
     *
     * @param <T> 这里压制了类型转换警告，需要自己注意泛型，避免产生类型转换异常
     * @return list, if endRow below of equals startLow will return a empty list
     * @throws ExcelException come from{{@link RowReadCallback#readRow(Row)}}
     */
    @NotNull
    @SuppressWarnings({Warning.RAW_TYPES, Warning.UNCHECKED})
    public <T> List<T> read() {
        logger.debug("read data in range: [{}, {}]", this.startRow, this.endRow);
        List res = new ArrayList();
        for (Row row : this) {
            // 不为空 && 定义的单元格数量大于 0
            if (row != null && row.getPhysicalNumberOfCells() > 0) {
                Object value = this.reader.readRow(row);
                if (value != null) {
                    res.add(value);
                }
            }
        }
        return res;
    }

    /**
     * 根据迭代器的顺序，二次处理解析后的数据
     *
     * @param consumer 数据二次加工，一般是数据持久化相关的操作。
     * @param <T>      这里压制了类型转换警告，需要自己注意泛型，避免产生类型转换异常
     * @throws ExcelException come from{{@link RowReadCallback#readRow(Row)}}
     */
    @SuppressWarnings({Warning.UNCHECKED})
    public <T> void eachRowDate(Consumer<T> consumer) {
        logger.debug("each data in range: [{}, {}]", this.startRow, this.endRow);
        for (Row row : this) {
            // 不为空 && 定义的单元格数量大于 0
            if (row != null && row.getPhysicalNumberOfCells() > 0) {
                Object value = this.reader.readRow(row);
                if (value != null) {
                    consumer.accept((T) value);
                }
            }
        }
    }

    /**
     * 根据迭代器的顺序，处理每一行数据，注意每一行可能为空。
     *
     * @param consumer 数据处理程序
     */
    public void eachRow(Consumer<Row> consumer) {
        logger.debug("each row in range: [{}, {}]", this.startRow, this.endRow);
        for (Row row : this) {
            consumer.accept(row);
        }
    }

    /**
     * 读取单行数据，兼容超大文件的内容读取
     *
     * @param <T> 这里压制了类型转换警告，需要自己注意泛型，避免产生类型转换异常
     * @return idx 行号
     * @throws ExcelException           come from{{@link RowReadCallback#readRow(Row)}}
     * @throws IllegalArgumentException 当前行无数据
     */
    @NotNull
    @SuppressWarnings({Warning.UNCHECKED})
    public <T> T readRow(int idx) {
        Row row = sheet.getRow(idx);
        Asserts.notNull(row, "Row number out of excel size: {}", idx);
        return (T) this.reader.readRow(row);
    }

    /**
     * 这种读取方式，只适用于小型 Excel。
     * <p>
     * Excel中没有段落的概念，可以用空行做区分。
     * 程序会不断读取到下一行，读取到空行，则返回这一段的内容。
     * 此时，指针移到前面一行的位置，方便下一次从该行号开始读取数据。
     *
     * @param <T> 这里压制了类型转换警告，需要自己注意泛型，避免产生类型转换异常
     * @return 如果读到结尾，返回 null，否则返回一个 list
     * @throws ExcelException come from{{@link RowReadCallback#readRow(Row)}}
     */
    @SuppressWarnings({Warning.RAW_TYPES, Warning.UNCHECKED})
    public <T> List<T> nextParagraph() {
        // 起止行号
        int last = this.endRow == null ? this.sheet.getLastRowNum() : this.endRow;
        int start = this.startRow;

        if (start <= last) {
            List res = new ArrayList();
            while (start < last) {
                Row row = sheet.getRow(start);
                if (row != null) {
                    Object value = this.reader.readRow(row);
                    if (value != null) {
                        res.add(value);
                    }
                } else {
                    break;
                }
                start++;
            }
            this.startRow = start;
            return res;
        } else {
            return null;
        }
    }

    /**
     * Write data into sheet，
     * <p>
     * 这里压制了类型转换警告，
     * 需要程序员自己处理 {@link RowReaderAndWriter} 与 {@link List} 的泛型，二者需要保持一致。
     *
     * @param list data
     * @return this
     */
    @SuppressWarnings({Warning.RAW_TYPES, Warning.UNCHECKED})
    public ExcelBook write(@NotNull List list) {
        // 写入数据
        int idx = this.startRow;
        for (Object t : list) {
            Row row = sheet.createRow(idx);
            this.writer.writeRow(row, t);
            idx++;
        }
        return this;
    }


    /**
     * 写入一行数据
     *
     * @param row  行号
     * @param data VO
     * @return this
     */
    @SuppressWarnings({Warning.UNCHECKED})
    public ExcelBook writeRow(int row, @NotNull Object data) {
        Row r = sheet.createRow(row);
        this.writer.writeRow(r, data);
        return this;
    }

    /**
     * 写入一行数据
     * <p>
     * String 类型，会保证程序输出与用户看到的内容完全一致，但是数字在 Excel 中会有警告，如果用于计算，需要手动转换格式。
     *
     * @param row  行号
     * @param data 数组
     * @return this
     */
    public ExcelBook writeArray(int row, @NotNull String[] data) {
        Row r = sheet.createRow(row);
        // 写入数据
        for (int i = 0; i < data.length; i++) {
            ExcelCell.addCell(r, i, data[i], this.style);
        }
        return this;
    }

    /**
     * 写入一行数据
     * <p>
     * Object 类型，程序会自动转成最合适的类型，最终效果可能会与预期不符
     *
     * @param row  行号
     * @param data 表头
     * @return this
     */
    public ExcelBook writeArray(int row, @NotNull Object[] data) {
        Row r = sheet.createRow(row);
        // 写入数据
        for (int i = 0; i < data.length; i++) {
            ExcelCell.addCell(r, i, data[i], this.style);
        }
        return this;
    }

    /**
     * 合并单元格，并写入标题
     *
     * @param title 标题
     * @param row   行号
     * @param span  占用的列
     * @return this
     */
    public ExcelBook writeTitle(int row, int span, @NotNull String title) {
        Cell cell = sheet.createRow(row).createCell(0);
        cell.setCellStyle(this.style);
        ExcelCell.setValue(cell, title);
        if (span > 1) {
            this.merge(row, row, 0, span - 1);
        }
        return this;
    }

    /**
     * 写入表头
     *
     * @param row     行号
     * @param headers 表头
     * @return this
     * @since Excel 2007
     */
    public ExcelBook writeHeader(int row, @NotNull Header[] headers) {
        Row r = sheet.createRow(row);
        for (int i = 0; i < headers.length; i++) {
            Header header = headers[i];
            Cell c = r.createCell(i);
            c.setCellStyle(this.style);
            c.setCellValue(header.getLabel());
        }
        return this;
    }

    /**
     * write excel content into output-stream
     *
     * @param os os
     * @throws IOException -
     */
    public void out(@NotNull OutputStream os) throws IOException {
        this.workbook.write(os);
    }

    /**
     * Return a Iterator that can traverse row of sheet between startRow and endRow.
     *
     * @return -
     */
    @NotNull
    @Override
    public Iterator<Row> iterator() {
        logger.debug("using iterator in range: [{}, {}]", this.startRow, this.endRow);

        // 不设置首末行，则使用默认迭代器
        if (this.startRow == null && this.endRow == null) {
            return this.sheet.iterator();
        }

        // 默认从首行开始读取
        if (this.startRow == null) {
            this.setFirstRowAsStartRow();
        }
        // 默认读取到尾行
        if (this.endRow == null) {
            this.setLastRowAsEndRow();
        }
        // 在起止行号之间迭代
        return new Iterator<Row>() {
            private int index = startRow;
            private final int end = endRow;

            @Override
            public boolean hasNext() {
                return index <= end;
            }

            @Override
            public Row next() {
                int row = index;
                index++;
                return sheet.getRow(row);
            }
        };
    }

    /**
     * Closes this stream and releases any system resources associated
     * with it. If the stream is already closed then invoking this
     * method has no effect.
     *
     * @throws IOException f an I/O error occurs
     */
    @Override
    public void close() throws IOException {
        if (this.workbook != null) {
            this.workbook.close();
        }
    }
}
